﻿//Copyright (c) Microsoft Corporation.  All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace Microsoft.Ted.Wacel.Base
{
    public class Cube : CloudDataStructure
    {
        private MasterDetails<string, string> mBatches;

        public Dictionary<string, Dimension> Dimensions;

        public void Clear()
        {
            MyDataProvider.Clear(); //TODO: Don't use region
            mBatches.Clear();
        }
        public Cube(string name, 
                    MasterDetails<string,string> batches,
                    IDataProvider dataProvider, 
                    ILockProvider lockProvider)
            : base(name, dataProvider, lockProvider)
        {
            Dimensions = new Dictionary<string, Dimension>();
            mBatches = batches;
        }
        public void AddDimension(Dimension dimension)
        {
            if (dimension == null)
                throw new ArgumentNullException("Dimensin paramter can't be null.");
            if (Dimensions.ContainsKey(dimension.Name))
                throw new ArgumentException("Dimension " + dimension.Name + " already exists.");

            Dimensions.Add(dimension.Name, dimension);
        }

        #region Update overloads

        public void Update(IEnumerable<dynamic> data)
        {
            Update(getDataPoints(data));
        }
        public void Update(params dynamic[] data)
        {
            Update((IEnumerable<dynamic>)data);
        }

        public void UpdateDelayRollup(string batchId, IEnumerable<dynamic> data)
        {
            UpdateDelayRollup(batchId, getDataPoints(data));
        }
        public void UpdateDelayRollup(string batchId, params dynamic[] data)
        {
            UpdateDelayRollup(batchId, (IEnumerable<dynamic>)data);
        }
        public void Update(IEnumerable<DataPoint> dataPoints)
        {
            var batchId = BeginBatch();
            mBatches.UpdateDetails(batchId, (from p in dataPoints select new KeyValuePair<string,string>(p.Key, p.Key)).ToList());
            update(dataPoints);
            RollupBatch(batchId);
            CompleteBatch(batchId);
        }
        public void UpdateDelayRollup(string batchId, IEnumerable<DataPoint> dataPoints)
        {
            updateBatchDetails(batchId, dataPoints);
            update(dataPoints);
        }
        #endregion

        private void updateBatchDetails(string batchId,  IEnumerable<DataPoint> dataPoints)
        {
            mBatches.MergeDetails(batchId, (from p in dataPoints select new KeyValuePair<string, string>(p.Key, p.Key)).ToList());
        }

        private void update(IEnumerable<DataPoint> dataPoints)
        {
            
            if (MyDataProvider.Configuration.WriteBatchSize <= 1)
            {
                Parallel.ForEach(dataPoints, (datapoint) =>
                {
                    Put(datapoint.Key, new NodeData(datapoint.Value) { Version = (datapoint.Version + 1) % (long.MaxValue - 1) });
                });
            }
            else
            {
                int pageIndex = 0;
                int numOfPages = (dataPoints.Count() + MyDataProvider.Configuration.WriteBatchSize - 1) / MyDataProvider.Configuration.WriteBatchSize;
                while (pageIndex < numOfPages)
                {
                    var page = dataPoints.Skip(pageIndex * MyDataProvider.Configuration.WriteBatchSize).Take(MyDataProvider.Configuration.WriteBatchSize);
                    List<KeyValuePair<string, object>> pageData = new List<KeyValuePair<string, object>>();
                    foreach(var datapoint in page)
                    {
                       pageData.Add(new KeyValuePair<string, object>(datapoint.Key, new NodeData(datapoint.Value) { Version = (datapoint.Version + 1) % (long.MaxValue - 1) }));
                    }
                    BulkPut(pageData);
                    pageIndex++;
                }
            }
        }

        private void rollup(IEnumerable<DataPoint> dataPoints)
        {
            rollupLayer(dataPoints);
        }

        private void rollupLayer(IEnumerable<DataPoint> dataPoints)
        {
            Dictionary<string, DataPoint> parentPoints = new Dictionary<string, DataPoint>();
            Parallel.ForEach(dataPoints, (datapoint) =>
            {
                for (int i = 0; i < datapoint.Coordinates.Count; i++)
                {
                    var node = datapoint.Coordinates[i];
                    if (node.Parent != null)
                    {
                        var data = datapoint.ReplaceCoordinate(FindNodeByName(datapoint.Coordinates[i].ParentDimension, node.Parent.Name));
                        lock (parentPoints)
                        {
                            if (!parentPoints.ContainsKey(data.Key))
                                parentPoints.Add(data.Key, data);
                        }
                    }
                }
            });
            if (parentPoints.Count() > 0)
            {
                Parallel.ForEach(parentPoints.Values, (datapoint) =>
                {
                    for (int i = 0; i < datapoint.Coordinates.Count; i++)
                    {
                        if (datapoint.Coordinates[i].Children.Count > 0)
                        {
                            var childrenData = Drilldown(datapoint, datapoint.Coordinates[i].ParentDimension.Name);
                            if (childrenData.Count > 0)
                            {
                                double[] data = new double[childrenData.Count];
                                double sum = 0;
                                for (int j = 0; j < childrenData.Count; j++)
                                {
                                    data[j] = childrenData[j].Value;
                                    sum += data[j];
                                }
                                Array.Sort(data);
                                NodeData nodeData = new NodeData
                                {
                                    Value = sum,
                                    Average = sum / data.Length,
                                    Min = data[0],
                                    Max = data[data.Length - 1],
                                    Median = (data.Length % 2 == 0 ? (data[data.Length / 2 - 1] + data[data.Length / 2]) / 2 : data[data.Length / 2]),
                                    Version = (datapoint.Version + 1) % (long.MaxValue - 1)
                                };

                                ProviderUpdate(datapoint.Key, nodeData);
                            }
                        }
                    }
                    });
                rollupLayer(parentPoints.Values);
            }
        }
        
        private List<DataPoint> getDataPoints(IEnumerable<dynamic> data)
        {
            var properties = data.ElementAt(0).GetType().GetProperties();
            List<string> axis = new List<string>();
            foreach (var property in properties)
                axis.Add(property.Name);
            axis.Sort();
            List<DataPoint> dataPoints = new List<DataPoint>();
            foreach (var element in data)
            {
                DataPoint dataPoint = new DataPoint();
                Type type = element.GetType();
                for (int i = 0; i < axis.Count; i++)
                {
                    if (axis[i] == "Value")
                        dataPoint.Value = type.GetProperty("Value").GetValue(element, null);
                    else
                    {
                        var value = type.GetProperty(axis[i]).GetValue(element, null);
                        if (axis[i] == "Time" && value is DateTime)
                        {
                            dataPoint.Coordinates.Add(FindNodeByName(Dimensions[axis[i]], Dimensions[axis[i]].GetTimeDimensionKey((DateTime)value)));
                        }
                        else
                            dataPoint.Coordinates.Add(FindNodeByName(Dimensions[axis[i]], value));
                    }
                }
                dataPoints.Add(dataPoint);
            }
            return dataPoints;
        }
        public void Rollup(IEnumerable<DataPoint> dataPoints)
        {
            rollup(dataPoints);
        }
        public DimensionNode FindNodeByName(DimensionNode node, string name)
        {
            if (node is Dimension && ((Dimension)node).DimensionType == DimensionType.Free)
                return new DimensionNode(name, (Dimension)node, DimensionNodeType.Independant);
            if (node.Name == name)
                return node;
            else
            {
                foreach (var child in node.Children)
                {
                    var cNode = FindNodeByName(child, name);
                    if (cNode != null)
                        return cNode;
                }
                return null;
            }
        }

        public DataPoint Query(dynamic query)
        {
            var properties = query.GetType().GetProperties();
            List<string> axis = new List<string>();
            foreach (var property in properties)
                axis.Add(property.Name);
            axis.Sort();
            DataPoint dataPoint = new DataPoint();
            Type type = query.GetType();
            for (int i = 0; i < axis.Count; i++)
            {
                if (axis[i] != "Value")
                    dataPoint.Coordinates.Add(FindNodeByName(Dimensions[axis[i]], type.GetProperty(axis[i]).GetValue(query, null)));
            }
            var val = Get(dataPoint.Key, typeHint:typeof(NodeData), typeHint2:typeof(double));
            if (val == null)
                dataPoint.Value = 0.0;
            else if (val is NodeData)
                dataPoint.SetNodeData((NodeData)val);
            else
                dataPoint.Value = (double)val;
            return dataPoint;
        }
        public List<DataPoint> Drilldown(DataPoint datapoint, string dimension)
        {
            Dictionary<string, DataPoint> ret = new Dictionary<string, DataPoint>();
            List<string> keys = new List<string>();
            for (int i = 0; i < datapoint.Coordinates.Count; i++)
            {
                if (datapoint.Coordinates[i].ParentDimension.Name == dimension)
                {
                    if (datapoint.Coordinates[i].Children.Count > 0)
                    {
                        foreach (var child in datapoint.Coordinates[i].Children)
                        {
                            var queried = datapoint.ReplaceCoordinate(child);
                            keys.Add(queried.Key);
                            ret.Add(queried.Key, queried);
                        }
                    }
                    break;
                }
            }
            var values = BulkGet(keys, typeHint:typeof(NodeData), typeHint2:typeof(double));
            foreach(var value in values)
            {
                var obj = value.Value;
                if (obj == null)
                    ret[value.Key].Value = 0.0;
                else if (obj is NodeData)
                    ret[value.Key].SetNodeData((NodeData)obj);
                else
                    ret[value.Key].Value = (double)obj;
            }
            return ret.Values.ToList();
        }
        public List<DataPoint> Query(dynamic start, dynamic end)
        {
            throw new NotImplementedException();
        }

        #region Batch handling
        private string mCurrentBatch = null;
        public string BeginBatch()
        {
            string batchId = Guid.NewGuid().ToString("D").Substring(0,8);
            mBatches.Add(batchId, batchId);
            mCurrentBatch = batchId;
            return batchId;
        }
        public void ResumeBatch(string batchId)
        {
            if (!string.IsNullOrEmpty(mBatches.SafeGet(batchId, null)))
                throw new ArgumentOutOfRangeException(string.Format("Batch '{0}' not found.", batchId));
            mCurrentBatch = batchId;
        }
        public void RollupBatch(string batchId)
        {
            var details = mBatches.GetDetails(batchId);
            if (details != null)
            {
                var list = from detail in details
                           select rehydrate(detail.Value);
                Rollup(list.ToList());
            }
        }
        private DataPoint rehydrate(string key)
        {
            //TODO: Note, rehydrate doesn't restore value
            string[] parts = key.Split(':');
            DataPoint dataPoint = new DataPoint();
            for (int i =0; i < parts.Length-1; i+=2)
                    dataPoint.Coordinates.Add(FindNodeByName(Dimensions[parts[i]], parts[i+1]));
            return dataPoint;
            
        }
        public void CompleteBatch(string batchId)
        {
            mBatches.Delete(batchId);
        }
        public void PutBatchTag(string key, object tag)
        {
            PutTag<object>(key, tag);
        }
        public object GetBatchTag(string key)
        {
            return GetTag<object>(key,null);
        }
        #endregion
    }
}
